package cn.qfys521.mc_websocket.client

import cn.qfys521.mc_websocket.client.models.*
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import java.net.URI
import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.int
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit

class ManagementWsClientSchemaCoverageTest {

    private fun clientNoReconnect() = ManagementWsClient(
        URI("ws://localhost:25585"),
        config = ManagementWsClient.ReconnectConfig(autoReconnect = false)
    )

    // ---- Request encoding (primitive + object) ----

    @Test
    fun `encode serversettings autosave set boolean`() {
        val c = clientNoReconnect()
        val obj: JsonObject = c.buildRequest(
            method = "minecraft:serversettings/autosave/set",
            id = 10,
            params = true,
            paramsSerializer = Boolean.serializer()
        )
        val params = obj["params"]!!.jsonArray
        assertEquals(1, params.size)
        assertTrue(params[0].jsonPrimitive.boolean)
    }

    @Test
    fun `encode serversettings max_players set int`() {
        val c = clientNoReconnect()
        val obj: JsonObject = c.buildRequest(
            method = "minecraft:serversettings/max_players/set",
            id = 11,
            params = 20,
            paramsSerializer = Int.serializer()
        )
        val p = obj["params"]!!.jsonArray[0].jsonPrimitive.int
        assertEquals(20, p)
    }

    @Test
    fun `encode serversettings motd set string`() {
        val c = clientNoReconnect()
        val obj: JsonObject = c.buildRequest(
            method = "minecraft:serversettings/motd/set",
            id = 12,
            params = "Hello",
            paramsSerializer = String.serializer()
        )
        val p = obj["params"]!!.jsonArray[0].jsonPrimitive.content
        assertEquals("Hello", p)
    }

    @Test
    fun `encode server system_message object`() {
        val c = clientNoReconnect()
        val msg = SystemMessage(
            overlay = true,
            message = Message(literal = "hi"),
            receivingPlayers = null
        )
        val obj: JsonObject = c.buildRequest(
            method = "minecraft:server/system_message",
            id = 13,
            params = msg,
            paramsSerializer = SystemMessage.serializer()
        )
        val first = obj["params"]!!.jsonArray[0].jsonObject
        assertEquals(true, first["overlay"]!!.jsonPrimitive.boolean)
        val m = first["message"]!!.jsonObject
        assertEquals("hi", m["literal"]!!.jsonPrimitive.content)
    }

    // ---- Response decoding (primitive + list + object) ----

    @Test
    fun `decode server stop boolean`() {
        val c = clientNoReconnect()
        val resp = """{"jsonrpc":"2.0","id":1,"result":true}"""
        val ok = c.decodeResultFromResponse(Boolean.serializer(), resp)
        assertTrue(ok)
    }

    @Test
    fun `decode server status object`() {
        val c = clientNoReconnect()
        val resp = """{"jsonrpc":"2.0","id":1,"result":{"players":[],"started":true,"version":{"protocol":767,"name":"1.x"}}}"""
        val st = c.decodeResultFromResponse(ServerState.serializer(), resp)
        assertTrue(st.started)
        assertEquals(767, st.version.protocol)
    }

    @Test
    fun `decode gamerules list`() {
        val c = clientNoReconnect()
        val resp = """{"jsonrpc":"2.0","id":1,"result":[{"key":"doDaylightCycle","type":"boolean","value":"true"}]}"""
        val rules = c.decodeResultFromResponse(ListSerializer(TypedGameRule.serializer()), resp)
        assertEquals(1, rules.size)
        assertEquals("doDaylightCycle", rules[0].key)
        assertEquals(TypedGameRule.RuleType.BOOLEAN, rules[0].type)
        assertEquals("true", rules[0].value)
    }

    @Test
    fun `decode gamerules update typed result`() {
        val c = clientNoReconnect()
        val resp = """{"jsonrpc":"2.0","id":1,"result":{"key":"randomTickSpeed","type":"integer","value":"3"}}"""
        val rule = c.decodeResultFromResponse(TypedGameRule.serializer(), resp)
        assertEquals("randomTickSpeed", rule.key)
        assertEquals(TypedGameRule.RuleType.INTEGER, rule.type)
        assertEquals("3", rule.value)
    }

    // ---- Error paths ----

    @Test
    fun `decode invalid json throws ParseError`() {
        val c = clientNoReconnect()
        assertThrows(ManagementException.ParseError::class.java) {
            c.decodeResultFromResponse(Boolean.serializer(), "not-json")
        }
    }

    @Test
    fun `decode missing result throws ParseError`() {
        val c = clientNoReconnect()
        assertThrows(ManagementException.ParseError::class.java) {
            c.decodeResultFromResponse(Boolean.serializer(), """{"jsonrpc":"2.0","id":1}""")
        }
    }

    // ---- Notification decoding coverage ----

    @Test
    fun `notify operators added`() {
        val c = clientNoReconnect()
        val fut = CompletableFuture<Operator?>()
        c.onNotification("notification:operators/added", Operator.serializer()) { fut.complete(it) }
        c.onMessage("""{"jsonrpc":"2.0","method":"notification:operators/added","params":[{"permissionLevel":4,"bypassesPlayerLimit":true,"player":{"id":"u","name":"n"}}]}""")
        val seen = fut.get(1, TimeUnit.SECONDS)
        assertNotNull(seen)
        assertEquals(4, seen!!.permissionLevel)
        assertTrue(seen.bypassesPlayerLimit!!)
        assertEquals("n", seen.player!!.name)
    }

    @Test
    fun `notify allowlist added`() {
        val c = clientNoReconnect()
        val fut = CompletableFuture<Player?>()
        c.onNotification("notification:allowlist/added", Player.serializer()) { fut.complete(it) }
        c.onMessage("""{"jsonrpc":"2.0","method":"notification:allowlist/added","params":[{"id":"u","name":"n"}]}""")
        val p = fut.get(1, TimeUnit.SECONDS)
        assertEquals("u", p!!.id)
    }

    @Test
    fun `notify ip_bans added`() {
        val c = clientNoReconnect()
        val fut = CompletableFuture<IpBan?>()
        c.onNotification("notification:ip_bans/added", IpBan.serializer()) { fut.complete(it) }
        c.onMessage("""{"jsonrpc":"2.0","method":"notification:ip_bans/added","params":[{"ip":"1.2.3.4"}]}""")
        val ip = fut.get(1, TimeUnit.SECONDS)
        assertEquals("1.2.3.4", ip!!.ip)
    }

    @Test
    fun `notify bans added`() {
        val c = clientNoReconnect()
        val fut = CompletableFuture<UserBan?>()
        c.onNotification("notification:bans/added", UserBan.serializer()) { fut.complete(it) }
        c.onMessage("""{"jsonrpc":"2.0","method":"notification:bans/added","params":[{"player":{"id":"u","name":"n"},"reason":"r"}]}""")
        val ub = fut.get(1, TimeUnit.SECONDS)
        assertEquals("r", ub!!.reason)
        assertEquals("n", ub.player.name)
    }

    @Test
    fun `notify gamerules updated`() {
        val c = clientNoReconnect()
        val fut = CompletableFuture<TypedGameRule?>()
        c.onNotification("notification:gamerules/updated", TypedGameRule.serializer()) { fut.complete(it) }
        c.onMessage("""{"jsonrpc":"2.0","method":"notification:gamerules/updated","params":[{"key":"k","type":"boolean","value":"false"}]}""")
        val gr = fut.get(1, TimeUnit.SECONDS)
        assertEquals("k", gr!!.key)
        assertEquals(TypedGameRule.RuleType.BOOLEAN, gr.type)
    }

    @Test
    fun `notify server status`() {
        val c = clientNoReconnect()
        val fut = CompletableFuture<ServerState?>()
        c.onNotification("notification:server/status", ServerState.serializer()) { fut.complete(it) }
        c.onMessage("""{"jsonrpc":"2.0","method":"notification:server/status","params":[{"players":[],"started":true,"version":{"protocol":767,"name":"1.x"}}]}""")
        val st = fut.get(1, TimeUnit.SECONDS)
        assertTrue(st!!.started)
        assertEquals(767, st.version.protocol)
    }
}
